Contacto:
Gaspar Cólogan Barajas
Jose Manuel de Castro Beristraín
Este trabajo consiste en analizar y prevenir el fraude bancario en base a una extracción de solicitud de apertura de una cuenta bancaria con un conjunto de datos anonimizados.
El conjunto de datos fue creado por Sérgio Jesus, José Pombal, Duarte Alves, André F. Cruz, Pedro Saleiro y Pedro Bizarro en nombre de Feedzai y fue financiado por la misma empresa. Consiste en seis conjuntos de datos. En esta práctica, utilizaremos unicamente los el conjunto de datos "base" donde no se introdujo sesgo en el proceso de muestreo.
Cada instancia del conjuntos de datos representa una aplicación sintética de la apertura apertura de una cuenta bancaria, generada utilizando un modelo CTGAN entrenado con un conjunto de datos real anonimizado para la detección de fraudes en la apertura de cuentas bancarias. La información original se obtuvo durante el proceso de solicitud con el consentimiento del usuario. No se realizaron revisiones éticas y la recopilación de datos no se realizó directamente de los individuos, sino a través del muestreo de un modelo CTGAN.
El etiquetado se realiza a través del campo "fraud_bool", donde un valor positivo (fraud_bool=1) indica una solicitud fraudulenta y un valor negativo (fraud_bool=0) indica una solicitud legítima. El conjunto de datos aborda la selección de etiquetas y presenta una pequeña selección sesgada debido a restricciones regulatorias y comerciales que limitan el tipo de clientes que algunos bancos pueden aceptar.
import pandas as pd
import numpy as np
import seaborn as sns
from matplotlib import pyplot as plt
import plotly.express as px
import warnings
import category_encoders as ce
from sklearn.preprocessing import OneHotEncoder
warnings.filterwarnings('ignore')
pd.set_option('display.max_columns', 500)
pd.set_option('display.max_rows', 5000)
def dame_variables_categoricas(dataset=None, max_valores_distintos=100):
'''
----------------------------------------------------------------------------------------------------------
Función dame_variables_categoricas:
----------------------------------------------------------------------------------------------------------
-Descripción: Función que recibe un dataset y devuelve una lista con los nombres de las
variables categóricas
-Inputs:
-- dataset: Pandas dataframe que contiene los datos
-- max_valores_distintos: Número máximo de valores distintos permitidos para considerar
una variable como categórica (por defecto 100)
-Return:
-- lista_variables_categoricas: lista con los nombres de las variables categóricas del
dataset de entrada con menos de max_valores_distintos valores diferentes
-- 1: la ejecución es incorrecta
'''
if dataset is None:
print(u'\nFaltan argumentos por pasar a la función')
return 1
lista_variables_categoricas = []
for columna in dataset.columns:
if pd.api.types.is_object_dtype(dataset[columna]):
# Si el tipo de dato es 'object' (categórico)
if len(dataset[columna].dropna().unique()) < max_valores_distintos:
lista_variables_categoricas.append(columna)
return lista_variables_categoricas
def plot_feature(df, col_name, isContinuous, target):
"""
Visualize a variable with and without faceting on the loan status.
- df dataframe
- col_name is the variable name in the dataframe
- full_name is the full variable name
- continuous is True if the variable is continuous, False otherwise
"""
f, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(12,3), dpi=90)
count_null = df[col_name].isnull().sum()
if isContinuous:
sns.histplot(df.loc[df[col_name].notnull(), col_name], kde=False, ax=ax1)
else:
sns.countplot(df[col_name], order=sorted(df[col_name].unique()), color='#5975A4', saturation=1, ax=ax1)
ax1.set_xlabel(col_name)
ax1.set_ylabel('Count')
ax1.set_title(col_name+ ' Numero de nulos: '+str(count_null))
plt.xticks(rotation = 90)
if isContinuous:
sns.boxplot(x=df[col_name], y=df[target], ax=ax2)
ax2.set_ylabel('')
ax2.set_title(col_name + ' by '+target)
else:
data = df.groupby(col_name)[target].value_counts(normalize=True).to_frame('proportion').reset_index()
data.columns = [i, target, 'proportion']
#sns.barplot(x = col_name, y = 'proportion', hue= target, data = data, saturation=1, ax=ax2)
sns.barplot(x = col_name, y = 'proportion', hue= target, data = data, saturation=1, ax=ax2)
ax2.set_ylabel(target+' fraction')
ax2.set_title(target)
plt.xticks(rotation = 90)
ax2.set_xlabel(col_name)
plt.tight_layout()
def get_corr_matrix(dataset = None, metodo='pearson', size_figure=[10,8]):
# Para obtener la correlación de Spearman, sólo cambiar el metodo por 'spearman'
if dataset is None:
print(u'\nHace falta pasar argumentos a la función')
return 1
sns.set(style="white")
# Compute the correlation matrix
corr = dataset.corr(method=metodo)
# Set self-correlation to zero to avoid distraction
for i in range(corr.shape[0]):
corr.iloc[i, i] = 0
# Set up the matplotlib figure
f, ax = plt.subplots(figsize=size_figure)
# Draw the heatmap with the mask and correct aspect ratio
sns.heatmap(corr, center=0,
square=True, linewidths=.5, cmap ='viridis' ) #cbar_kws={"shrink": .5}
plt.show()
return 0
def get_deviation_of_mean_perc(pd_loan, list_var_continuous, target, multiplier):
pd_final = pd.DataFrame()
for i in list_var_continuous:
series_mean = pd_loan[i].mean()
series_std = pd_loan[i].std()
std_amp = multiplier * series_std
left = series_mean - std_amp
right = series_mean + std_amp
size_s = pd_loan[i].size
perc_goods = pd_loan[i][(pd_loan[i] >= left) & (pd_loan[i] <= right)].size / size_s
perc_excess = pd_loan[i][(pd_loan[i] < left) | (pd_loan[i] > right)].size / size_s
if perc_excess > 0:
pd_concat_percent = pd_loan[target][(pd_loan[i] < left) | (pd_loan[i] > right)] \
.value_counts(normalize=True).reset_index()
if not pd_concat_percent.empty:
pd_concat_percent.columns = ['value', 'percentage']
pd_concat_percent = pd_concat_percent[pd_concat_percent['value'] != 0]
pd_concat_percent['variable'] = i
pd_concat_percent['sum_outlier_values'] = pd_loan[i][(pd_loan[i] < left) | (pd_loan[i] > right)].size
pd_concat_percent['porcentaje_sum_null_values'] = perc_excess
pd_final = pd.concat([pd_final, pd_concat_percent], axis=0).reset_index(drop=True)
if pd_final.empty:
print('No existen variables con valores nulos')
print()
return pd_final
def get_percent_null_values_target(pd_loan, list_var_continuous, target):
pd_final = pd.DataFrame()
for i in list_var_continuous:
if pd_loan[i].isnull().sum()>0:
pd_concat_percent = pd.DataFrame(pd_loan[target][pd_loan[i].isnull()]\
.value_counts(normalize=True).reset_index()).T
pd_concat_percent.columns = [pd_concat_percent.iloc[0,0],
pd_concat_percent.iloc[0,1]]
pd_concat_percent = pd_concat_percent.drop('index',axis=0)
pd_concat_percent['variable'] = i
pd_concat_percent['sum_null_values'] = pd_loan[i].isnull().sum()
pd_concat_percent['porcentaje_sum_null_values'] = pd_loan[i].isnull().sum()/pd_loan.shape[0]
pd_final = pd.concat([pd_final, pd_concat_percent], axis=0).reset_index(drop=True)
if pd_final.empty:
print('No existen variables con valores nulos')
return pd_final
def distribucion_categoricas(df, variable, target='fraud_bool'):
result = df.groupby([variable, target]).size().unstack(fill_value=0)
result['relative_frequency_0'] = result['0'] / (result['0'] + result['1'])
result['relative_frequency_1'] = result['1'] / (result['0'] + result['1'])
# Mostrar el resultado
display(result)
df_fraud = pd.read_csv("../data/Base.csv")
df_fraud.head()
| fraud_bool | income | name_email_similarity | prev_address_months_count | current_address_months_count | customer_age | days_since_request | intended_balcon_amount | payment_type | zip_count_4w | velocity_6h | velocity_24h | velocity_4w | bank_branch_count_8w | date_of_birth_distinct_emails_4w | employment_status | credit_risk_score | email_is_free | housing_status | phone_home_valid | phone_mobile_valid | bank_months_count | has_other_cards | proposed_credit_limit | foreign_request | source | session_length_in_minutes | device_os | keep_alive_session | device_distinct_emails_8w | device_fraud_count | month | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 0.9 | 0.166828 | -1 | 88 | 50 | 0.020925 | -1.331345 | AA | 769 | 10650.765523 | 3134.319630 | 3863.647740 | 1 | 6 | CA | 185 | 0 | BA | 1 | 0 | 24 | 0 | 500.0 | 0 | INTERNET | 3.888115 | windows | 0 | 1 | 0 | 7 |
| 1 | 1 | 0.9 | 0.296286 | -1 | 144 | 50 | 0.005418 | -0.816224 | AB | 366 | 534.047319 | 2670.918292 | 3124.298166 | 718 | 3 | CA | 259 | 1 | BA | 0 | 0 | 15 | 0 | 1500.0 | 0 | INTERNET | 31.798819 | windows | 0 | 1 | 0 | 7 |
| 2 | 1 | 0.9 | 0.044985 | -1 | 132 | 40 | 3.108549 | -0.755728 | AC | 870 | 4048.534263 | 2893.621498 | 3159.590679 | 1 | 14 | CB | 177 | 1 | BA | 0 | 1 | -1 | 0 | 200.0 | 0 | INTERNET | 4.728705 | other | 0 | 1 | 0 | 7 |
| 3 | 1 | 0.9 | 0.159511 | -1 | 22 | 50 | 0.019079 | -1.205124 | AB | 810 | 3457.064063 | 4054.908412 | 3022.261812 | 1921 | 6 | CA | 110 | 1 | BA | 0 | 1 | 31 | 1 | 200.0 | 0 | INTERNET | 2.047904 | linux | 0 | 1 | 0 | 7 |
| 4 | 1 | 0.9 | 0.596414 | -1 | 218 | 50 | 0.004441 | -0.773276 | AB | 890 | 5020.341679 | 2728.237159 | 3087.670952 | 1990 | 2 | CA | 295 | 1 | BA | 1 | 0 | 31 | 0 | 1500.0 | 0 | INTERNET | 3.775225 | macintosh | 1 | 1 | 0 | 7 |
En primer lugar, leemos nuestro archivo CSV y hacemos un .head con el fin de tener una visión global de nuestro dataframe. Es cierto que no obtenemos ninguna información relevante ni adicional de los datos, no obstante, nos permite tener una idea general de los datos con los que estamos tratando.
df_fraud.columns
Index(['fraud_bool', 'income', 'name_email_similarity',
'prev_address_months_count', 'current_address_months_count',
'customer_age', 'days_since_request', 'intended_balcon_amount',
'payment_type', 'zip_count_4w', 'velocity_6h', 'velocity_24h',
'velocity_4w', 'bank_branch_count_8w',
'date_of_birth_distinct_emails_4w', 'employment_status',
'credit_risk_score', 'email_is_free', 'housing_status',
'phone_home_valid', 'phone_mobile_valid', 'bank_months_count',
'has_other_cards', 'proposed_credit_limit', 'foreign_request', 'source',
'session_length_in_minutes', 'device_os', 'keep_alive_session',
'device_distinct_emails_8w', 'device_fraud_count', 'month'],
dtype='object')
df_fraud.dtypes.sort_values().to_frame('feature_type').groupby(by = 'feature_type').size().to_frame('count').reset_index()
| feature_type | count | |
|---|---|---|
| 0 | int64 | 18 |
| 1 | float64 | 9 |
| 2 | object | 5 |
dimension=df_fraud.shape, df_fraud.drop_duplicates().shape
dimension
((1000000, 32), (1000000, 32))
Podemos observar que nuestro dataframe contiene 1.000.000 de instancias distribuidas en 32 columnas. Estas columnas, son principalmente de tipo int64 ya que 18 son de este tipo, 9 son float64 y 5 tipo object.
df_fraud
| fraud_bool | income | name_email_similarity | prev_address_months_count | current_address_months_count | customer_age | days_since_request | intended_balcon_amount | payment_type | zip_count_4w | velocity_6h | velocity_24h | velocity_4w | bank_branch_count_8w | date_of_birth_distinct_emails_4w | employment_status | credit_risk_score | email_is_free | housing_status | phone_home_valid | phone_mobile_valid | bank_months_count | has_other_cards | proposed_credit_limit | foreign_request | source | session_length_in_minutes | device_os | keep_alive_session | device_distinct_emails_8w | device_fraud_count | month | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 0.9 | 0.166828 | -1 | 88 | 50 | 0.020925 | -1.331345 | AA | 769 | 10650.765523 | 3134.319630 | 3863.647740 | 1 | 6 | CA | 185 | 0 | BA | 1 | 0 | 24 | 0 | 500.0 | 0 | INTERNET | 3.888115 | windows | 0 | 1 | 0 | 7 |
| 1 | 1 | 0.9 | 0.296286 | -1 | 144 | 50 | 0.005418 | -0.816224 | AB | 366 | 534.047319 | 2670.918292 | 3124.298166 | 718 | 3 | CA | 259 | 1 | BA | 0 | 0 | 15 | 0 | 1500.0 | 0 | INTERNET | 31.798819 | windows | 0 | 1 | 0 | 7 |
| 2 | 1 | 0.9 | 0.044985 | -1 | 132 | 40 | 3.108549 | -0.755728 | AC | 870 | 4048.534263 | 2893.621498 | 3159.590679 | 1 | 14 | CB | 177 | 1 | BA | 0 | 1 | -1 | 0 | 200.0 | 0 | INTERNET | 4.728705 | other | 0 | 1 | 0 | 7 |
| 3 | 1 | 0.9 | 0.159511 | -1 | 22 | 50 | 0.019079 | -1.205124 | AB | 810 | 3457.064063 | 4054.908412 | 3022.261812 | 1921 | 6 | CA | 110 | 1 | BA | 0 | 1 | 31 | 1 | 200.0 | 0 | INTERNET | 2.047904 | linux | 0 | 1 | 0 | 7 |
| 4 | 1 | 0.9 | 0.596414 | -1 | 218 | 50 | 0.004441 | -0.773276 | AB | 890 | 5020.341679 | 2728.237159 | 3087.670952 | 1990 | 2 | CA | 295 | 1 | BA | 1 | 0 | 31 | 0 | 1500.0 | 0 | INTERNET | 3.775225 | macintosh | 1 | 1 | 0 | 7 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 999995 | 0 | 0.6 | 0.192631 | -1 | 104 | 40 | 0.030592 | -1.044454 | AB | 804 | 7905.711839 | 8341.468557 | 4972.635997 | 1 | 8 | CA | 75 | 1 | BC | 1 | 1 | 25 | 0 | 200.0 | 0 | INTERNET | 8.511502 | linux | 1 | 1 | 0 | 4 |
| 999996 | 0 | 0.8 | 0.322989 | 148 | 9 | 50 | 1.628119 | -1.409803 | AC | 3306 | 5391.470463 | 4955.170808 | 5022.728108 | 0 | 2 | CC | 154 | 1 | BC | 1 | 1 | -1 | 0 | 200.0 | 0 | INTERNET | 8.967865 | windows | 0 | 1 | 0 | 4 |
| 999997 | 0 | 0.8 | 0.879403 | -1 | 30 | 20 | 0.018563 | 34.692760 | AA | 1522 | 8063.102636 | 5670.654316 | 4377.196321 | 2023 | 6 | CF | 64 | 0 | BC | 0 | 1 | 11 | 0 | 200.0 | 0 | INTERNET | 8.195531 | other | 0 | 1 | 0 | 4 |
| 999998 | 0 | 0.9 | 0.762112 | -1 | 189 | 20 | 0.015352 | 94.661055 | AA | 1418 | 8092.641762 | 3982.582204 | 4394.803296 | 1678 | 6 | CA | 163 | 0 | BA | 1 | 0 | 28 | 0 | 500.0 | 0 | INTERNET | 4.336064 | windows | 1 | 1 | 0 | 4 |
| 999999 | 0 | 0.2 | 0.697452 | -1 | 321 | 20 | 2.655916 | 9.908499 | AA | 951 | 6169.630036 | 3695.308261 | 4352.334543 | 2 | 12 | CA | 36 | 1 | BE | 0 | 1 | 15 | 0 | 200.0 | 0 | INTERNET | 6.717022 | linux | 0 | 1 | 0 | 4 |
1000000 rows × 32 columns
df_fraud.describe()
| fraud_bool | income | name_email_similarity | prev_address_months_count | current_address_months_count | customer_age | days_since_request | intended_balcon_amount | zip_count_4w | velocity_6h | velocity_24h | velocity_4w | bank_branch_count_8w | date_of_birth_distinct_emails_4w | credit_risk_score | email_is_free | phone_home_valid | phone_mobile_valid | bank_months_count | has_other_cards | proposed_credit_limit | foreign_request | session_length_in_minutes | keep_alive_session | device_distinct_emails_8w | device_fraud_count | month | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 1000000.000000 | 1000000.000000 | 1000000.000000 | 1000000.000000 | 1000000.000000 | 1000000.000000 | 1.000000e+06 | 1000000.000000 | 1000000.000000 | 1000000.000000 | 1000000.000000 | 1000000.000000 | 1000000.000000 | 1000000.000000 | 1000000.000000 | 1000000.000000 | 1000000.000000 | 1000000.000000 | 1000000.000000 | 1000000.000000 | 1000000.000000 | 1000000.000000 | 1000000.000000 | 1000000.000000 | 1000000.000000 | 1000000.0 | 1000000.000000 |
| mean | 0.011029 | 0.562696 | 0.493694 | 16.718568 | 86.587867 | 33.689080 | 1.025705e+00 | 8.661499 | 1572.692049 | 5665.296605 | 4769.781965 | 4856.324016 | 184.361849 | 9.503544 | 130.989595 | 0.529886 | 0.417077 | 0.889676 | 10.839303 | 0.222988 | 515.851010 | 0.025242 | 7.544940 | 0.576947 | 1.018312 | 0.0 | 3.288674 |
| std | 0.104438 | 0.290343 | 0.289125 | 44.046230 | 88.406599 | 12.025799 | 5.381835e+00 | 20.236155 | 1005.374565 | 3009.380665 | 1479.212612 | 919.843934 | 459.625329 | 5.033792 | 69.681812 | 0.499106 | 0.493076 | 0.313293 | 12.116875 | 0.416251 | 487.559902 | 0.156859 | 8.033106 | 0.494044 | 0.180761 | 0.0 | 2.209994 |
| min | 0.000000 | 0.100000 | 0.000001 | -1.000000 | -1.000000 | 10.000000 | 4.036860e-09 | -15.530555 | 1.000000 | -170.603072 | 1300.307314 | 2825.748405 | 0.000000 | 0.000000 | -170.000000 | 0.000000 | 0.000000 | 0.000000 | -1.000000 | 0.000000 | 190.000000 | 0.000000 | -1.000000 | 0.000000 | -1.000000 | 0.0 | 0.000000 |
| 25% | 0.000000 | 0.300000 | 0.225216 | -1.000000 | 19.000000 | 20.000000 | 7.193246e-03 | -1.181488 | 894.000000 | 3436.365848 | 3593.179135 | 4268.368423 | 1.000000 | 6.000000 | 83.000000 | 0.000000 | 0.000000 | 1.000000 | -1.000000 | 0.000000 | 200.000000 | 0.000000 | 3.103053 | 0.000000 | 1.000000 | 0.0 | 1.000000 |
| 50% | 0.000000 | 0.600000 | 0.492153 | -1.000000 | 52.000000 | 30.000000 | 1.517574e-02 | -0.830507 | 1263.000000 | 5319.769349 | 4749.921161 | 4913.436941 | 9.000000 | 9.000000 | 122.000000 | 1.000000 | 0.000000 | 1.000000 | 5.000000 | 0.000000 | 200.000000 | 0.000000 | 5.114321 | 1.000000 | 1.000000 | 0.0 | 3.000000 |
| 75% | 0.000000 | 0.800000 | 0.755567 | 12.000000 | 130.000000 | 40.000000 | 2.633069e-02 | 4.984176 | 1944.000000 | 7680.717827 | 5752.574191 | 5488.083356 | 25.000000 | 13.000000 | 178.000000 | 1.000000 | 1.000000 | 1.000000 | 25.000000 | 0.000000 | 500.000000 | 0.000000 | 8.866131 | 1.000000 | 1.000000 | 0.0 | 5.000000 |
| max | 1.000000 | 0.900000 | 0.999999 | 383.000000 | 428.000000 | 90.000000 | 7.845690e+01 | 112.956928 | 6700.000000 | 16715.565404 | 9506.896596 | 6994.764201 | 2385.000000 | 39.000000 | 389.000000 | 1.000000 | 1.000000 | 1.000000 | 32.000000 | 1.000000 | 2100.000000 | 1.000000 | 85.899143 | 1.000000 | 2.000000 | 0.0 | 7.000000 |
El conjunto de datos comprende un total de 1,000,000 registros, cada uno caracterizando diversas aplicaciones bancarias. Una de las variables clave es "fraud_bool", una variable binaria que indica si una aplicación es fraudulenta o no. En promedio, alrededor del 1.10% de las aplicaciones en el conjunto de datos se clasifican como fraudulentas.
En cuanto a los perfiles de los solicitantes, la variable "income" revela que los ingresos anuales, expresados en deciles, tienen una media de aproximadamente 0.56. Esto sugiere una distribución variada de ingresos en la población de solicitantes. Además, la métrica "name_email_similarity" proporciona un valor promedio de alrededor del 0.49, indicando la similitud entre el nombre y el correo electrónico del solicitante.
Explorando los detalles de las direcciones, la variable "prev_address_months_count" indica que, en promedio, los solicitantes han residido alrededor de 16.72 meses en su dirección anterior, con una considerable variabilidad representada por una desviación estándar de 44.05 meses. Por otro lado, la variable "current_address_months_count" muestra que, en promedio, los solicitantes han vivido aproximadamente 86.59 meses en su dirección actual, con una desviación estándar de 88.41 meses.
La edad media de los solicitantes en el conjunto de datos es de aproximadamente 33.69 años, redondeada a la década más cercana. Este dato sugiere que la población de solicitantes es relativamente joven. La edad mínima es de 10 años, y la máxima es de 90 años, reflejando una amplia diversidad en la edad de los solicitantes.
Por último, el número de Aplicaciones Fraudulentas con el Mismo Dispositivo (device_fraud_count), Con una media de aproximadamente 0.00, esta variable indica la frecuencia promedio de aplicaciones fraudulentas asociadas al mismo dispositivo en las últimas 8 semanas. La mayoría de las aplicaciones no parecen tener asociaciones fraudulentas con el dispositivo utilizado.
df_fraud_fraud_bool = df_fraud['fraud_bool']\
.value_counts(normalize=True)\
.mul(100).rename('percent').reset_index()
df_fraud_fraud_bool_conteo = df_fraud['fraud_bool'].value_counts().reset_index()
df_fraud_fraud_bool_pc = pd.merge(df_fraud_fraud_bool, df_fraud_fraud_bool_conteo, how='inner')
df_fraud_fraud_bool_pc
| fraud_bool | percent | count | |
|---|---|---|---|
| 0 | 0 | 98.8971 | 988971 |
| 1 | 1 | 1.1029 | 11029 |
import plotly.express as px
# Suponiendo que 'index' es una columna en df_fraud_fraud_bool_pc
fig = px.histogram(df_fraud_fraud_bool_pc, x=df_fraud_fraud_bool_pc.index, y=['percent'])
fig.show()
La variable objetivo "fraud_bool", representa con 1 los casos donde ha habido fraude bancario y con 0 representa los casos que son legítimos. En este dataframe, nos encontramos con un 1,10 % de casos fraudulentos, lo que representan aproximadamente 11.000 instancias del millón que posee.
Tal y como se indicó en el diccionario de datos, algunas variables contienen nulos que están definidos con valores negativos. A continuación, vamos a analizar la cantidad de datos negativos que poseen cada una de estas columnas y analizaremos si es relevante mantener esta información o es favorable omitirla.
# Lista de columnas con valores negativos o -1
columns_with_negatives = [
'prev_address_months_count',
'current_address_months_count',
'intended_balcon_amount',
'bank_months_count',
'session_length_in_minutes',
'device_distinct_emails_8w'
]
# Filtrar el DataFrame para incluir solo las columnas de interés
df_negatives = df_fraud[columns_with_negatives]
# Convertir columnas a números si es posible
df_negatives = df_negatives.apply(pd.to_numeric, errors='coerce')
# Contar los valores negativos o iguales a -1 por cada columna
negatives_count_per_column = df_negatives.lt(0).sum()
# Calcular el porcentaje de valores negativos en cada columna
total_values_per_column = df_negatives.count()
percent_negatives_per_column = (negatives_count_per_column / total_values_per_column)
# Crear DataFrame con los resultados
result_df = pd.DataFrame({
'negativos_columnas': negatives_count_per_column,
'porcentaje_negativos_columnas': percent_negatives_per_column
})
# Ordenar el DataFrame por el porcentaje de mayor a menor
result_df = result_df.sort_values(by='porcentaje_negativos_columnas', ascending=False)
# Imprimir el resultado
display(result_df)
| negativos_columnas | porcentaje_negativos_columnas | |
|---|---|---|
| intended_balcon_amount | 742523 | 0.742523 |
| prev_address_months_count | 712920 | 0.712920 |
| bank_months_count | 253635 | 0.253635 |
| current_address_months_count | 4254 | 0.004254 |
| session_length_in_minutes | 2015 | 0.002015 |
| device_distinct_emails_8w | 359 | 0.000359 |
Observamos que 2 de nuestras variables intended_balcon_amount y prev_address_months_count tienen un alto porcentaje de valores faltantes, en ambos casos supera el 70%. En el caso de la columna bank_months_count un cuarto de los valores son nulos. Y encontramos también otras tres variables con uno número bastante escaso de valores nulos.
A continuación, vamos a hacer una análisis más exhaustivo de estos valores nulos para determinar si podemos eliminar estos registros. En concreto, vamos a ver si hay algún valor añadido que podamos concluir a raíz de un valor nulo, centrandonos en la relación nulo con el fraude bancario.
# Lista de columnas con valores negativos o -1
columns_with_negatives = [
'prev_address_months_count',
'current_address_months_count',
'intended_balcon_amount',
'bank_months_count',
'session_length_in_minutes',
'device_distinct_emails_8w'
]
# Filtrar el DataFrame para incluir solo las columnas de interés
df_negatives = df_fraud[columns_with_negatives + ['fraud_bool']]
# Convertir columnas a números si es posible
df_negatives[columns_with_negatives] = df_negatives[columns_with_negatives].apply(pd.to_numeric, errors='coerce')
# Contar los valores nulos (menores a 0):
negatives_count_per_column = df_negatives[columns_with_negatives].lt(0).sum()
positive_count_per_column = df_negatives[columns_with_negatives].ge(0).sum()
# Crear DataFrame con los resultados
result_df = pd.DataFrame({
'columna': columns_with_negatives,
'total_nulos': negatives_count_per_column,
'nulos_0': df_negatives[df_negatives['fraud_bool'] == 0][columns_with_negatives].lt(0).sum(),
'nulos_1': df_negatives[df_negatives['fraud_bool'] == 1][columns_with_negatives].lt(0).sum(),
'total_no_nulos': positive_count_per_column,
'no_nulos_0': df_negatives[df_negatives['fraud_bool'] == 0][columns_with_negatives].ge(0).sum(),
'no_nulos_1': df_negatives[df_negatives['fraud_bool'] == 1][columns_with_negatives].ge(0).sum(),
})
# Calcular porcentajes de valores nulos
result_df['porcentaje_nulos_0'] = (result_df['nulos_0'] / result_df['total_nulos']) * 100
result_df['porcentaje_nulos_1'] = (result_df['nulos_1'] / result_df['total_nulos']) * 100
# Calcular porcentajes de valores no nulos
result_df['porcentaje_no_nulos_0'] = (result_df['no_nulos_0'] / (result_df['no_nulos_0'] + result_df['no_nulos_1'])) * 100
result_df['porcentaje_no_nulos_1'] = (result_df['no_nulos_1'] / (result_df['no_nulos_0'] + result_df['no_nulos_1'])) * 100
# Reiniciar el índice para evitar duplicados
result_df.reset_index(drop=True, inplace=True)
# Reorganizar las columnas para tener una estructura más clara
result_df = result_df[['columna', 'total_nulos', 'nulos_0', 'nulos_1',
'porcentaje_nulos_0', 'porcentaje_nulos_1', 'total_no_nulos',
'no_nulos_0', 'no_nulos_1',
'porcentaje_no_nulos_0', 'porcentaje_no_nulos_1']]
# Imprimir el resultado
display(result_df)
| columna | total_nulos | nulos_0 | nulos_1 | porcentaje_nulos_0 | porcentaje_nulos_1 | total_no_nulos | no_nulos_0 | no_nulos_1 | porcentaje_no_nulos_0 | porcentaje_no_nulos_1 | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | prev_address_months_count | 712920 | 702786 | 10134 | 98.578522 | 1.421478 | 287080 | 286185 | 895 | 99.688240 | 0.311760 |
| 1 | current_address_months_count | 4254 | 4240 | 14 | 99.670898 | 0.329102 | 995746 | 984731 | 11015 | 98.893794 | 1.106206 |
| 2 | intended_balcon_amount | 742523 | 732776 | 9747 | 98.687313 | 1.312687 | 257477 | 256195 | 1282 | 99.502091 | 0.497909 |
| 3 | bank_months_count | 253635 | 249495 | 4140 | 98.367733 | 1.632267 | 746365 | 739476 | 6889 | 99.076993 | 0.923007 |
| 4 | session_length_in_minutes | 2015 | 1997 | 18 | 99.106700 | 0.893300 | 997985 | 986974 | 11011 | 98.896677 | 1.103323 |
| 5 | device_distinct_emails_8w | 359 | 355 | 4 | 98.885794 | 1.114206 | 999641 | 988616 | 11025 | 98.897104 | 1.102896 |
La tabla anterior muestra con valores absolutos y porcentuales cuantos de nuestros valores nulos conllevan fraude bancario y cuantos no. Gracias a un análisis previo, determinamos que aproximadamente 1,10 % de nuestro dataset era fraude bancario y el 98,9 % es legítimo, por lo que en cuanto más próximo se encuentre la distribución del fraude individualmente en cada variable, significa que no nos aporta ningún valor añadido los números nulos.
La variable "intended_balcon_amount" es la que mayor representación de valores nulos tiene. Vemos que el 98,69% de los valores nulos no conllevan fraude bancario, y por ende, el 1,31% de los nulos si son fraude bancario. En el caso de los valores no nulos, 99.50 % de los datos no son fraude bancario y aproximadamente el 0,5% de los datos sí son fraude. Es decir, En caso de que tengamos un valor nulo dentro de la variable "intended_balcon_amount", hay más del doble de posibilidad que nos encontremos ante un caso de fraude bancario.
En caso de nuestra segunda variable con más nulos, "prev_address_months_count", observamos que el 1,42% de nuestros valores nulos son fraude bancario. Este porcentaje es aproximadamente 4 veces más grande que el que nos encontramos en el caso de los valores no nulos, el cual posee un 0,31% de fraude bancario.
En la columna llamada "bank_months_count", también observamos un porcentaje de fraude superior para los datos nulos (1,63%), que en caso de los datos sin nulos, donde hay un 0,92% de casos fraudulentos.
Por lo tanto, podemos concluir que estos valores nulos nos aportan un gran valor añadido a nuestro dataframe ya que en las variables con un alto índice de datos faltantes, la probabilidad de encontrarnos con fraude es superior a la distribución global de nuestro dataframe (aproximadamente 1,10%), y es más probable encontrarnos con fraude bancario en caso de tener un valor nulo que en caso de no tenerlo. Por lo tanto, hemos decidido mantener estos datos en nuestro dataframe y no eliminarlos. El resto de las variables que poseen algún dato nulo tampoco van a ser manipuladas ya que su representación es escasa e indiferente para nuestro análisis.
df_fraud.dtypes
fraud_bool int64 income float64 name_email_similarity float64 prev_address_months_count int64 current_address_months_count int64 customer_age int64 days_since_request float64 intended_balcon_amount float64 payment_type object zip_count_4w int64 velocity_6h float64 velocity_24h float64 velocity_4w float64 bank_branch_count_8w int64 date_of_birth_distinct_emails_4w int64 employment_status object credit_risk_score int64 email_is_free int64 housing_status object phone_home_valid int64 phone_mobile_valid int64 bank_months_count int64 has_other_cards int64 proposed_credit_limit float64 foreign_request int64 source object session_length_in_minutes float64 device_os object keep_alive_session int64 device_distinct_emails_8w int64 device_fraud_count int64 month int64 dtype: object
list_var_cat = dame_variables_categoricas(dataset=df_fraud)
df_fraud[list_var_cat] = df_fraud[list_var_cat].astype("category")
list_var_continuous = list(df_fraud.select_dtypes('float').columns)
df_fraud[list_var_continuous] = df_fraud[list_var_continuous].astype(float)
df_fraud.dtypes
fraud_bool int64 income float64 name_email_similarity float64 prev_address_months_count int64 current_address_months_count int64 customer_age int64 days_since_request float64 intended_balcon_amount float64 payment_type category zip_count_4w int64 velocity_6h float64 velocity_24h float64 velocity_4w float64 bank_branch_count_8w int64 date_of_birth_distinct_emails_4w int64 employment_status category credit_risk_score int64 email_is_free int64 housing_status category phone_home_valid int64 phone_mobile_valid int64 bank_months_count int64 has_other_cards int64 proposed_credit_limit float64 foreign_request int64 source category session_length_in_minutes float64 device_os category keep_alive_session int64 device_distinct_emails_8w int64 device_fraud_count int64 month int64 dtype: object
Nuestra variable objetivo la podemos en tipo "string":
df_fraud['fraud_bool'] = df_fraud['fraud_bool'].astype(str)
Seleccionamos unicamente las columnas de tipo númericas para la representación gráfica:
numeric_columns = df_fraud.select_dtypes(include=['float64', 'int64']).columns
# Crear un nuevo DataFrame con las variables numéricas
df_selected = df_fraud[numeric_columns]
distribucion_categoricas(df_fraud, 'payment_type')
distribucion_categoricas(df_fraud, 'employment_status')
distribucion_categoricas(df_fraud, 'housing_status')
distribucion_categoricas(df_fraud, 'source')
distribucion_categoricas(df_fraud, 'device_os')
| fraud_bool | 0 | 1 | relative_frequency_0 | relative_frequency_1 |
|---|---|---|---|---|
| payment_type | ||||
| AA | 256885 | 1364 | 0.994718 | 0.005282 |
| AB | 366385 | 4169 | 0.988749 | 0.011251 |
| AC | 247862 | 4209 | 0.983302 | 0.016698 |
| AD | 117551 | 1286 | 0.989178 | 0.010822 |
| AE | 288 | 1 | 0.996540 | 0.003460 |
| fraud_bool | 0 | 1 | relative_frequency_0 | relative_frequency_1 |
|---|---|---|---|---|
| employment_status | ||||
| CA | 721353 | 8899 | 0.987814 | 0.012186 |
| CB | 137335 | 953 | 0.993109 | 0.006891 |
| CC | 36826 | 932 | 0.975316 | 0.024684 |
| CD | 26422 | 100 | 0.996230 | 0.003770 |
| CE | 22640 | 53 | 0.997664 | 0.002336 |
| CF | 43949 | 85 | 0.998070 | 0.001930 |
| CG | 446 | 7 | 0.984547 | 0.015453 |
| fraud_bool | 0 | 1 | relative_frequency_0 | relative_frequency_1 |
|---|---|---|---|---|
| housing_status | ||||
| BA | 163318 | 6357 | 0.962534 | 0.037466 |
| BB | 259397 | 1568 | 0.993992 | 0.006008 |
| BC | 369855 | 2288 | 0.993852 | 0.006148 |
| BD | 25935 | 226 | 0.991361 | 0.008639 |
| BE | 168553 | 582 | 0.996559 | 0.003441 |
| BF | 1662 | 7 | 0.995806 | 0.004194 |
| BG | 251 | 1 | 0.996032 | 0.003968 |
| fraud_bool | 0 | 1 | relative_frequency_0 | relative_frequency_1 |
|---|---|---|---|---|
| source | ||||
| INTERNET | 982035 | 10917 | 0.989006 | 0.010994 |
| TELEAPP | 6936 | 112 | 0.984109 | 0.015891 |
| fraud_bool | 0 | 1 | relative_frequency_0 | relative_frequency_1 |
|---|---|---|---|---|
| device_os | ||||
| linux | 330997 | 1715 | 0.994845 | 0.005155 |
| macintosh | 53074 | 752 | 0.986029 | 0.013971 |
| other | 340754 | 1974 | 0.994240 | 0.005760 |
| windows | 256999 | 6507 | 0.975306 | 0.024694 |
| x11 | 7147 | 81 | 0.988794 | 0.011206 |
Las tablas anteriores nos permiten ver como se distribuye el fraude dentro de las variables que son tipo "category".
import time
start_time = time.time()
target = 'fraud_bool' # Define tu columna objetivo aquí
for col_name in df_selected.columns:
print(col_name)
plot_feature(df_fraud, col_name=col_name, isContinuous=True, target=target)
end_time = time.time()
elapsed_time = end_time - start_time
print(f'Tiempo total de ejecución: {elapsed_time} segundos')
income name_email_similarity prev_address_months_count current_address_months_count customer_age days_since_request intended_balcon_amount zip_count_4w velocity_6h velocity_24h velocity_4w bank_branch_count_8w date_of_birth_distinct_emails_4w credit_risk_score email_is_free phone_home_valid phone_mobile_valid bank_months_count has_other_cards proposed_credit_limit foreign_request session_length_in_minutes keep_alive_session device_distinct_emails_8w device_fraud_count month Tiempo total de ejecución: 145.17566680908203 segundos
Los gráficos anteriores realizados a partir de todas las variables numéricas representan la distribución individual de cada variable y la distribución con respecto a nuestra variable objetivo "fraud_bool". A continuación, vamos a comentar cada uno de estos gráficos:
#Revertimos el cambio y volvemos a poner fraud_bool como int64
df_fraud['fraud_bool'] = df_fraud['fraud_bool'].astype(np.int64)
Vamos a analizar los valores atípicos con el fin de determinar si los eliminamos o mantenemos en nuestro conjunto de datos:
list_var_continuous = list(df_fraud.select_dtypes('float').columns)
get_deviation_of_mean_perc(df_fraud, list_var_continuous, target='fraud_bool', multiplier=3)
| value | percentage | variable | sum_outlier_values | porcentaje_sum_null_values | |
|---|---|---|---|---|---|
| 0 | 1 | 0.011758 | days_since_request | 17775 | 0.017775 |
| 1 | 1 | 0.009705 | intended_balcon_amount | 18960 | 0.018960 |
| 2 | 1 | 0.006450 | velocity_6h | 4341 | 0.004341 |
| 3 | 1 | 0.003711 | velocity_24h | 539 | 0.000539 |
| 4 | 1 | 0.129651 | proposed_credit_limit | 6155 | 0.006155 |
| 5 | 1 | 0.020048 | session_length_in_minutes | 23593 | 0.023593 |
Identificamos los valores atípicos (outliers) en variables continuas en un conjunto de datos, y luego calcular la proporción de casos de fraude (fraud_bool = 1) dentro de esos valores atípicos. Vamos a analizar los resultados obtenidos:
Variable 'days_since_request':
Variable 'intended_balcon_amount':
Variable 'velocity_6h':
Variable 'velocity_24h':
Variable 'proposed_credit_limit':
Variable 'session_length_in_minutes':
Finalmente, hemos decidido mantener los outliers dentro de nuestro dataset debido al bajo porcentaje que representan.
Por último, analizamos las correlaciones entre variables continuas en tu conjunto de datos:
get_corr_matrix(dataset = df_fraud[list_var_continuous],
metodo='pearson', size_figure=[10,8])
0
corr = df_fraud[list_var_continuous].corr('pearson')
new_corr = corr.abs()
new_corr.loc[:,:] = np.tril(new_corr, k=-1)
new_corr = new_corr.stack().to_frame('correlation').reset_index().sort_values(by='correlation', ascending=False)
new_corr[new_corr['correlation']>0.4]
| level_0 | level_1 | correlation | |
|---|---|---|---|
| 59 | velocity_4w | velocity_24h | 0.539115 |
| 49 | velocity_24h | velocity_6h | 0.464003 |
| 58 | velocity_4w | velocity_6h | 0.400254 |
Observamos una correlación positiva entre las diferentes variables de "velocity":
Correlación entre 'velocity_4w' y 'velocity_24h':
Correlación entre 'velocity_24h' y 'velocity_6h':
Correlación entre 'velocity_4w' y 'velocity_6h':